<?php
// +-----------------------------------------------------------------------------------------------
// | 简易CMS
// +-----------------------------------------------------------------------------------------------
// | [请手动修改文件描述]
// +-----------------------------------------------------------------------------------------------
// | Author: IT果农 <htmambo@163.com> <http://www.haolie.net>
// +-----------------------------------------------------------------------------------------------
// | Version $Id: WechatAuth.class.php 9 2016-09-17 11:22:17Z IT果农 <htmambo@163.com> $
// +-----------------------------------------------------------------------------------------------

namespace CMS\Wechat;

use CMS\Wechat\WechatErrCode;
use CMS\Wechat\Auth;
/**
 * 微信SDK授权处理
 */
class WechatAuth extends Auth {
    /* 消息类型常量 */
    const MSG_TYPE_TEXT = 'text';
    const MSG_TYPE_IMAGE = 'image';
    const MSG_TYPE_VOICE = 'voice';
    const MSG_TYPE_VIDEO = 'video';
    const MSG_TYPE_SHORTVIDEO = 'shortvideo';
    const MSG_TYPE_LOCATION = 'location';
    const MSG_TYPE_LINK = 'link';
    const MSG_TYPE_MUSIC = 'music';
    const MSG_TYPE_NEWS = 'news';
    const MSG_TYPE_EVENT = 'event';

    /* 二维码类型常量 */
    const QR_SCENE = 'QR_SCENE';
    const QR_LIMIT_SCENE = 'QR_LIMIT_SCENE';

    /**
     * 微信api根路径
     * @var string
     */
    var $apiURL = 'https://api.weixin.qq.com/cgi-bin';

    /**
     * 微信二维码根路径
     * @var string
     */
    private $qrcodeURL = 'https://mp.weixin.qq.com/cgi-bin';
    private $oauthApiURL = 'https://api.weixin.qq.com/sns';

    /**
     * 获取access_token，用于后续接口访问
     * @return array access_token信息，包含 token 和有效期
     */
    public function getAccessToken($code = null, $force = false) {
        $param = array(
            'appid'  => $this->appId,
            'secret' => $this->appSecret
        );
        $token = false;
        $cachekey = 'wechat_token_' . $this->appId . '_wechat';
        if (!$force) {
            $token = cache($cachekey);
            if (!$token || $token['expires_in'] <= NOW_TIME) {
                $token = false;
            }
        }
        if (!$token) {
            $param['grant_type'] = 'client_credential';
            $url = "{$this->apiURL}/token";
            $token = self::http($url, $param);
            $token = json_decode($token, true);
            if (is_array($token) && isset($token['errcode'])) {
                throw new \Exception($token['errmsg']);
            }
            $token['expires_in'] += NOW_TIME - 100;       //缓存时间，这个由微信返回，这里加上当前时间戳
            cache($cachekey, $token);
        }
        if (is_array($token)) {
            $this->accessToken = $token['access_token'];
            return $token['access_token'];
        } else {
            throw new \Exception('获取微信access_token失败！');
        }
    }

    /**
     * 群发消息
     * @param type $mst
     * @param type $type
     * @return type
     */
    public function massSendAll($msg = '', $type = 'mpnews') {
        //基础数据
        $data = array(
            'filter' => array(
                'is_to_all' => 'true'
            )
        );
        if (is_array($msg)) {
            $data[$type] = $msg;
        } else {
            $data[$type] = array('media_id' => $msg);
        }
        $data['msgtype'] = $type;
        return $this->api('message/mass/sendall', $data);
    }

    public function massPreview($msg = '', $type = 'mpnews', $openid = '') {
        //基础数据
        if (is_array($msg)) {
            $data[$type] = $msg;
        } else {
            $data[$type] = array('media_id' => $msg);
        }
        $data['msgtype'] = $type;
        $data['touser'] = $openid;
        return $this->api('message/mass/preview', $data);
    }

    /**
     * 获取授权用户信息
     * @param  string $openid 用户的OpenID
     * @param  string $lang   指定的语言
     * @return array          用户信息数据，具体参见微信文档
     */
    public function getUserInfo($openid, $lang = 'zh_CN') {
        if (!$this->accessToken) {
            $this->accessToken = $this->getAccessToken();
        }
        $query = array(
            'access_token' => $this->accessToken,
            'openid'       => $openid,
            'lang'         => $lang,
        );
        return $this->api('user/info', '', 'GET', $query);
    }

    /**
     * 上传零时媒体资源
     * @param  string $filename 媒体资源本地路径
     * @param  string $type     媒体资源类型，具体请参考微信开发手册
     */
    public function mediaUpload($filename, $type) {
        $filename = realpath($filename);
        if (!$filename)
            throw new \Exception('资源路径错误！');

        $data = array(
            'type'  => $type,
            'media' => "@{$filename}"
        );

        return $this->api('media/upload', $data, 'POST', '', false);
    }

    /**
     * 获取已经保存的素材总数
     * @param string $type 素材的类型，图片（image）、视频（video）、语音（voice）、图文（news）
     */
    public function materialGetCount($type = 'all') {
        $types = array('image', 'video', 'voice', 'news', 'all');
        if (!in_array($type, $types)) {
            throw new \Exception('无效的素材类型！' . $type);
        }
        $result = $this->api('material/get_materialcount', '', 'POST', '', false);
        if ($type == 'all') {
            return $result;
        } else {
            return $result[$type . '_count'];
        }
    }

    /**
     * 获取已经保存的群发素材列表
     * @param string $type 素材的类型，图片（image）、视频（video）、语音（voice）、图文（news）
     * @param type $offset 从全部素材的该偏移位置开始返回，0表示从第一个素材返回
     * @param type $count 返回素材的数量，取值在1到20之间
     */
    public function materialGetList($type = 'news', $offset = 0, $count = 10) {
        $types = array('image', 'video', 'voice', 'news');
        if (!in_array($type, $types)) {
            throw new \Exception('无效的素材类型！' . $type);
        }
        $offset = max(0, intval($offset));
        $count = max(1, min(20, abs(intval($count))));
        $data = array(
            'type'   => $type,
            'offset' => $offset,
            'count'  => $count
        );
        return $this->api('material/batchget_material', $data, 'POST', '', true);
    }

    /**
     * 上传永久媒体资源
     * @param string $filename    媒体资源本地路径
     * @param string $type        媒体资源类型，具体请参考微信开发手册
     * @param string $description 资源描述，仅资源类型为 video 时有效
     */
    public function materialAddMaterial($filename, $type, $description = '') {
        $filename = realpath($filename);
        if (!$filename)
            throw new \Exception('资源路径错误！');

        $data = array(
            'type'  => $type,
            'media' => "@{$filename}",
        );

        if ($type == 'video') {
            if (is_array($description)) {
                //保护中文，微信api不支持中文转义的json结构
                array_walk_recursive($description, function(&$value) {
                    $value = urlencode($value);
                });
                $description = urldecode(json_encode($description));
            }
            $data['description'] = $description;
        }
        return $this->api('material/add_material', $data, 'POST', '', false);
    }

    /**
     * 获取媒体资源下载地址
     * 注意：视频资源不允许下载
     * @param  string $media_id 媒体资源id
     * @return string           媒体资源下载地址
     */
    public function mediaGet($media_id) {
        $param = array(
            'access_token' => $this->accessToken,
            'media_id'     => $media_id
        );

        $url = "{$this->apiURL}/media/get?";
        return $url . http_build_query($param);
    }

    /**
     * 给指定用户推送信息
     * 注意：微信规则只允许给在48小时内给公众平台发送过消息的用户推送信息
     * @param  string $openid  用户的openid
     * @param  array  $content 发送的数据，不同类型的数据结构可能不同
     * @param  string $type    推送消息类型
     */
    public function messageCustomSend($openid, $content, $type = self::MSG_TYPE_TEXT) {

        //基础数据
        $data = array(
            'touser'  => $openid,
            'msgtype' => $type,
        );

        //根据类型附加额外数据
        $data[$type] = call_user_func(array(self, $type), $content);

        return $this->api('message/custom/send', $data);
    }

    /**
     * 发送文本消息
     * @param  string $openid 用户的openid
     * @param  string $text   发送的文字
     */
    public function sendText($openid, $text) {
        return $this->messageCustomSend($openid, $text, self::MSG_TYPE_TEXT);
    }

    /**
     * 发送图片消息
     * @param  string $openid 用户的openid
     * @param  string $media  图片ID
     */
    public function sendImage($openid, $media) {
        return $this->messageCustomSend($openid, $media, self::MSG_TYPE_IMAGE);
    }

    /**
     * 发送语音消息
     * @param  string $openid 用户的openid
     * @param  string $media  音频ID
     */
    public function sendVoice($openid, $media) {
        return $this->messageCustomSend($openid, $media, self::MSG_TYPE_VOICE);
    }

    /**
     * 发送视频消息
     * @param  string $openid      用户的openid
     * @param  string $media_id    视频ID
     * @param  string $title       视频标题
     * @param  string $discription 视频描述
     */
    public function sendVideo() {
        $video = func_get_args();
        $openid = array_shift($video);
        return $this->messageCustomSend($openid, $video, self::MSG_TYPE_VIDEO);
    }

    /**
     * 发送音乐消息
     * @param  string $openid         用户的openid
     * @param  string $title          音乐标题
     * @param  string $discription    音乐描述
     * @param  string $musicurl       音乐链接
     * @param  string $hqmusicurl     高品质音乐链接
     * @param  string $thumb_media_id 缩略图ID
     */
    public function sendMusic() {
        $music = func_get_args();
        $openid = array_shift($music);
        return $this->messageCustomSend($openid, $music, self::MSG_TYPE_MUSIC);
    }

    /**
     * 发送图文消息
     * @param  string $openid 用户的openid
     * @param  array  $news   图文内容 [标题，描述，URL，缩略图]
     * @param  array  $news1  图文内容 [标题，描述，URL，缩略图]
     * @param  array  $news2  图文内容 [标题，描述，URL，缩略图]
     *                ...     ...
     * @param  array  $news9  图文内容 [标题，描述，URL，缩略图]
     */
    public function sendNews() {
        $news = func_get_args();
        $openid = array_shift($news);
        return $this->messageCustomSend($openid, $news, self::MSG_TYPE_NEWS);
    }

    /**
     * 发送一条图文消息
     * @param  string $openid      用户的openid
     * @param  string $title       文章标题
     * @param  string $discription 文章简介
     * @param  string $url         文章连接
     * @param  string $picurl      文章缩略图
     */
    public function sendNewsOnce() {
        $news = func_get_args();
        $openid = array_shift($news);
        $news = array($news);
        return $this->messageCustomSend($openid, $news, self::MSG_TYPE_NEWS);
    }

    /**
     * 创建用户组
     * @param  string $name 组名称
     */
    public function groupsCreate($name) {
        $data = array('group' => array('name' => $name));
        return $this->api('groups/create', $data);
    }

    /**
     * 查询所有分组
     * @return array 分组列表
     */
    public function groupsGet() {
        return $this->api('groups/get', '', 'GET');
    }

    /**
     * 查询用户所在的分组
     * @param  string $openid 用户的OpenID
     * @return number         分组ID
     */
    public function groupsGetid($openid) {
        $data = array('openid' => $openid);
        return $this->api('groups/getid', $data);
    }

    /**
     * 修改分组
     * @param  number $id   分组ID
     * @param  string $name 分组名称
     * @return array        修改成功或失败信息
     */
    public function groupsUpdate($id, $name) {
        $data = array('id' => $id, 'name' => $name);
        return $this->api('groups/update', $data);
    }

    /**
     * 移动用户分组
     * @param  string $openid     用户的OpenID
     * @param  number $to_groupid 要移动到的分组ID
     * @return array              移动成功或失败信息
     */
    public function groupsMemberUpdate($openid, $to_groupid) {
        $data = array('openid' => $openid, 'to_groupid' => $to_groupid);
        return $this->api('groups/member/update', $data);
    }

    /**
     * 用户设备注名
     * @param  string $openid 用户的OpenID
     * @param  string $remark 设备注名
     * @return array          执行成功失败信息
     */
    public function userInfoUpdateremark($openid, $remark) {
        $data = array('openid' => $openid, 'remark' => $remark);
        return $this->api('user/info/updateremark', $data);
    }

    /**
     * 获取指定用户的详细信息
     * @param  string $openid 用户的openid
     * @param  string $lang   需要获取数据的语言
     */
    public function userInfo($openid, $lang = 'zh_CN') {
        $param = array('openid' => $openid, 'lang' => $lang);
        return $this->api('user/info', '', 'GET', $param);
    }

    public function userInfoBatch($openids = array()) {
        array_walk_recursive($openids, function(&$v) {
            $v = array(
                'openid' => $v,
                //'lang' => 'zh-CN'
            );
        });
        $data = array(
            'user_list' => $openids
        );
        return $this->api('user/info/batchget', $data);
    }

    /**
     * 获取关注者列表
     * @param  string $next_openid 下一个openid，在用户数大于10000时有效
     * @return array               用户列表
     */
    public function userGet($next_openid = '') {
        $param = array('next_openid' => $next_openid);
        return $this->api('user/get', '', 'GET', $param);
    }

    /**
     * 创建自定义菜单
     * @param  array $button 符合规则的菜单数组，规则参见微信手册
     */
    public function menuCreate($button) {
        $data = array('button' => $button);
        return $this->api('menu/create', $data);
    }

    /**
     * 获取所有的自定义菜单
     * @return array  自定义菜单数组
     */
    public function menuGet() {
        return $this->api('menu/get', '', 'GET');
    }

    /**
     * 删除自定义菜单
     */
    public function menuDelete() {
        return $this->api('menu/delete', '', 'GET');
    }

    /**
     * 创建自定义菜单
     * @param  array $button 符合规则的菜单数组，规则参见微信手册
     */
    public function advMenuCreate($button = array(), $matchrule = array()) {
        $data = array('button' => $button, 'matchrule' => $matchrule);
        return $this->api('menu/addconditional', $data);
    }

    /**
     * 删除自定义菜单
     */
    public function advMenuDelete($menuid = '') {
        if($menuid) {
            $data = array(
                'menuid' => $menuid
            );
        } else {
            return false;
        }
        return $this->api('menu/delconditional', $data);
    }

    /**
     * 获取所有的自定义菜单
     * @return array  自定义菜单数组
     */
    public function advMenuTry($match = array()) {
        return $this->api('menu/trymatch', $match);
    }

    /**
     * 创建二维码，可创建指定有效期的二维码和永久二维码
     * @param  integer $scene_id       二维码参数
     * @param  integer $expire_seconds 二维码有效期，0-永久有效
     */
    public function qrcodeCreate($scene_id, $expire_seconds = 0) {
        $data = array();

        if (is_numeric($expire_seconds) && $expire_seconds > 0) {
            $data['expire_seconds'] = $expire_seconds;
            $data['action_name'] = self::QR_SCENE;
        } else {
            $data['action_name'] = self::QR_LIMIT_SCENE;
        }

        $data['action_info']['scene']['scene_id'] = $scene_id;
        return $this->api('qrcode/create', $data);
    }

    /**
     * 根据ticket获取二维码URL
     * @param  string $ticket 通过 qrcodeCreate接口获取到的ticket
     * @return string         二维码URL
     */
    public function showqrcode($ticket) {
        return "{$this->qrcodeURL}/showqrcode?ticket={$ticket}";
    }

    /**
     * 长链接转短链接
     * @param  string $long_url 长链接
     * @return string           短链接
     */
    public function shorturl($long_url) {
        $data = array(
            'action'   => 'long2short',
            'long_url' => $long_url
        );

        return $this->api('shorturl', $data);
    }

    /**
     * 构造文本信息
     * @param  string $content 要回复的文本
     */
    private static function text($content) {
        $data['content'] = $content;
        return $data;
    }

    /**
     * 构造图片信息
     * @param  integer $media 图片ID
     */
    private static function image($media) {
        $data['media_id'] = $media;
        return $data;
    }

    /**
     * 构造音频信息
     * @param  integer $media 语音ID
     */
    private static function voice($media) {
        $data['media_id'] = $media;
        return $data;
    }

    /**
     * 构造视频信息
     * @param  array $video 要回复的视频 [视频ID，标题，说明]
     */
    private static function video($video) {
        $data = array();
        list(
            $data['media_id'],
            $data['title'],
            $data['description'],
            ) = $video;

        return $data;
    }

    /**
     * 构造音乐信息
     * @param  array $music 要回复的音乐[标题，说明，链接，高品质链接，缩略图ID]
     */
    private static function music($music) {
        $data = array();
        list(
            $data['title'],
            $data['description'],
            $data['musicurl'],
            $data['hqmusicurl'],
            $data['thumb_media_id'],
            ) = $music;

        return $data;
    }

    /**
     * 构造图文信息
     * @param  array $news 要回复的图文内容
     * [
     *      0 => 第一条图文信息[标题，说明，图片链接，全文连接]，
     *      1 => 第二条图文信息[标题，说明，图片链接，全文连接]，
     *      2 => 第三条图文信息[标题，说明，图片链接，全文连接]，
     * ]
     */
    private static function news($news) {
        $articles = array();
        foreach ($news as $key => $value) {
            list(
                $articles[$key]['title'],
                $articles[$key]['description'],
                $articles[$key]['url'],
                $articles[$key]['picurl']
                ) = $value;

            if ($key >= 9)
                break; //最多只允许10条图文信息
        }

        $data['articles'] = $articles;
        return $data;
    }

}
